package flexo.utils;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.DefaultListModel;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.Timer;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import flexo.file.Node;

public class SpriteGenerator extends JFrame implements ActionListener,MouseListener,ListSelectionListener
{
	class Animation implements Comparable<Animation>
	{
		String name;
		List<Integer> frames;
		
		Animation(String name)
		{
			this.name = name;
			frames = new LinkedList<Integer>();
		}
		
		public String toString()
		{
			StringBuffer sb = new StringBuffer(name);
			if (frames.size() == 0)
			{
				sb.append(" > No frames");
			}
			else
			{
				for (Integer i : frames)
				{
					sb.append(" > " + i.toString());
				}
			}
			return sb.toString();
		}
		
		@Override		
		public int compareTo(Animation a)
		{
			return name.compareTo(a.name);
		}
	}
	
	class SpriteSheet
	{
		String name;
		int texWidth;
		int texHeight;
		int frameWidth;
		int frameHeight;
		int xFrames;
		int yFrames;
		int fps;
		List<Animation> animationList = new ArrayList<Animation>();
		
		public Animation getAnimation(String name)
		{
			for (Animation a : animationList)
			{
				if (a.name.equals(name))
				{
					return a;
				}
			}
			return null;
		}
		
		public boolean addAnimation(String name)
		{
			if (getAnimation(name) != null)
			{
				return false;
			}
			
			Animation a = new Animation(name);
			animationList.add(a);
			java.util.Collections.sort(animationList);
			
			return true;
		}
		
		public void removeAnimation(String name)
		{
			Animation rem = null;
			for (Animation a : animationList)
			{
				if (a.name.equals(name))
				{
					rem = a;
				}
			}
			if (rem != null)
			{
				animationList.remove(rem);
			}
		}
		
		public String toString()
		{
			return "texWidth="+texWidth+"\ntexHeight="+texHeight+"\nframeWidth="+frameWidth+"\nrfameHeight="+frameHeight+"\nxFrames="+xFrames+"\nyFrames="+yFrames;
		}
	} 
	
	private static final long serialVersionUID = 1L;	
	
	private JScrollPane previewPanel;
	private JLabel preview;
	private JLabel viewport;
	private final JFileChooser fc = new JFileChooser();
	
	private JTextField frameWidthField;
	private JTextField frameHeightField;
	private JTextField frameNumField;
	private JButton generateButton;
	private JButton loadButton;
	private JButton saveButton;
	private JButton addAnimButton;
	private JButton removeAnimButton;
	private JList animList;
	private JTextField statusText;
	private JButton statusReset;
	private javax.swing.Timer timer;
	private JButton speedIncrease;
	private JButton speedDecrease;
	
	private int previewFrameNumber;
	
	private SpriteSheet spritesheet;
	
	private BufferedImage previewImage = null;

	public SpriteGenerator()
	{
		this.setTitle("FLEXO Sprite Generator");
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		
		JPanel rootPanel = new JPanel();
		rootPanel.setLayout(new BorderLayout());
		this.getContentPane().add(rootPanel);
		
		viewport = new JLabel();			
		viewport.setAlignmentX(0.0f);
		viewport.setAlignmentY(0.0f);
		viewport.setOpaque(true);
		viewport.setBackground(Color.BLACK);
		viewport.setVerticalAlignment(JLabel.TOP);
		viewport.addMouseListener(this);
		
		JScrollPane scrollPanel = new JScrollPane(viewport);	
		scrollPanel.setPreferredSize(new Dimension(550,550));		
		
		JPanel widgetPanel = new JPanel();
		widgetPanel.setLayout(new BoxLayout(widgetPanel,BoxLayout.Y_AXIS));
		
		JPanel pre = new JPanel(new BorderLayout());
		pre.add(scrollPanel,BorderLayout.CENTER);
		JPanel status = new JPanel();
		pre.add(status,BorderLayout.SOUTH);
		
		createStatusBar(status);
		
		rootPanel.add(pre,BorderLayout.CENTER);
		rootPanel.add(widgetPanel,BorderLayout.EAST);		
		
		createGeneratePanel(widgetPanel);
		createModifyPanel(widgetPanel);
		
		enableButtons(false);		
		
		timer = new Timer(1000/4,this);
		
		spritesheet = new SpriteSheet();
		
		// Calculate the size of the form
		this.pack();
		// And center it on the screen
		this.setLocationRelativeTo(null);
		this.setVisible(true);
	}
	
	private void createStatusBar(JPanel panel)
	{
		panel.setLayout(new BorderLayout());
		
		statusText = new JTextField();
		statusText.setEditable(false);
		panel.add(statusText,BorderLayout.CENTER);
		
		statusReset = new JButton("Reset");
		statusReset.addActionListener(this);
		panel.add(statusReset,BorderLayout.EAST);
	}
	
	private void enableButtons(boolean enabled)
	{
		this.addAnimButton.setEnabled(enabled);
		this.removeAnimButton.setEnabled(enabled);
		this.saveButton.setEnabled(enabled);
		this.statusReset.setEnabled(enabled);
	}
	
	/**
	 * Fast method of retrieving the next power-of-2 next to a given number 'n'.
	 * If you pass a power-of-2 number, it returns the same number.
	 */
	public static int getNextPowOfTwo(int n)
	{
		--n;
		n |= n >> 16;
		n |= n >> 8;
		n |= n >> 4;
		n |= n >> 2;
		n |= n >> 1;
		++n;
		return n;
	}
	
	private void initSpriteSheet()
	{
		spritesheet.fps = 1;
		spritesheet.name = null;
		spritesheet.animationList.clear();
		spritesheet.frameHeight = Integer.parseInt(this.frameHeightField.getText());
		spritesheet.frameWidth = Integer.parseInt(this.frameWidthField.getText());
		int maxFrames = Integer.parseInt(this.frameNumField.getText());
		
		spritesheet.xFrames = 1;
		spritesheet.yFrames = 1;
		
		spritesheet.texWidth = 16;
		spritesheet.texHeight = 16;
		
		boolean sizeFound = false;
		
		while (!sizeFound)
		{
			// Update the size calculated until now
			if (spritesheet.texWidth <= spritesheet.texHeight)
			{
				spritesheet.texWidth = getNextPowOfTwo(spritesheet.texWidth+1);
			}
			else
			{
				spritesheet.texHeight = getNextPowOfTwo(spritesheet.texHeight+1);
			}
			
			// Update the max characters that fit in that size
			spritesheet.xFrames = spritesheet.texWidth / spritesheet.frameWidth;
			spritesheet.yFrames = spritesheet.texHeight / spritesheet.frameHeight;
			
			// Check if all the characters wanted by the user fit there
			if ( (spritesheet.xFrames * spritesheet.yFrames) >= maxFrames)
			{
				sizeFound = true;
			}
		}
	}
	
	private void generateImage()
	{		
		initSpriteSheet();
		
		// If there is no image, or the size has changed
		// we must create a new image
		if (previewImage == null || previewImage.getWidth() != spritesheet.texWidth || previewImage.getHeight() != spritesheet.texHeight)
		{
			// We remove the old image
			if (previewImage != null)
			{
				Graphics2D gfx = (Graphics2D)previewImage.getGraphics();
				gfx.dispose();
			}
			previewImage = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration().createCompatibleImage(spritesheet.texWidth, spritesheet.texHeight);
		}
		// Create the image
		Graphics2D gfx = (Graphics2D)previewImage.getGraphics();
		
		// Paint the rectangle with background color
		gfx.setBackground(Color.BLACK);	
		gfx.clearRect(0, 0, spritesheet.texWidth, spritesheet.texHeight);		
		
		// Draw the tiles
		for (int i = 0; i < spritesheet.yFrames; ++i)
		{
			int mult = 0;
			if (i % 2 != 0)
			{
				mult = 1;
			}
			for (int j = 0; j < spritesheet.xFrames; ++j)
			{				
				if ((j + mult) % 2 == 0)
				{
					gfx.setBackground(Color.MAGENTA);
				}
				else
				{
					gfx.setBackground(Color.WHITE);
				}
				gfx.clearRect(j * spritesheet.frameWidth,i * spritesheet.frameHeight,spritesheet.frameWidth,spritesheet.frameHeight);
			}
		}
		
		updateImageIcon();
	}
	
	private void updateImageIcon()
	{
		// Update the image on the form's panel
		ImageIcon imgPrev = new ImageIcon(previewImage);
		viewport.setIcon(imgPrev);
		// update preview size
		previewPanel.setPreferredSize(new Dimension(spritesheet.frameWidth,spritesheet.frameHeight));
		preview.setIcon(imgPrev);
		this.validate();
	}
	
	private void createGeneratePanel(JPanel widgetPanel)
	{
		JPanel generatePanel = new JPanel();
		generatePanel.setLayout(new BoxLayout(generatePanel,BoxLayout.Y_AXIS));
		generatePanel.setBorder(BorderFactory.createEtchedBorder());
			
		JPanel frameWidthPanel = new JPanel();
		JLabel frameWidthLabel = new JLabel();
		frameWidthLabel.setText("Frame width");
		frameWidthField = new JTextField();
		frameWidthField.setText("100");
		frameWidthPanel.add(frameWidthLabel);
		frameWidthPanel.add(frameWidthField);
		generatePanel.add(frameWidthPanel);
	
		JPanel frameHeightPanel = new JPanel();
		JLabel frameHeightLabel = new JLabel();
		frameHeightLabel.setText("Frame height");
		frameHeightField = new JTextField();
		frameHeightField.setText("150");
		frameHeightPanel.add(frameHeightLabel);
		frameHeightPanel.add(frameHeightField);
		generatePanel.add(frameHeightPanel);
		
		JPanel frameNumPanel = new JPanel();
		JLabel frameNumLabel = new JLabel();
		frameNumLabel.setText("Number of frames");
		frameNumField = new JTextField();
		frameNumField.setText("100");
		frameNumPanel.add(frameNumLabel);
		frameNumPanel.add(frameNumField);
		generatePanel.add(frameNumPanel);
		
		JPanel generateButtonsPanel = new JPanel();
		generateButton = new JButton();
		generateButton.setText("Generate");
		generateButton.addActionListener(this);
		generateButtonsPanel.add(generateButton);
		generatePanel.add(generateButtonsPanel);
		
		widgetPanel.add(generatePanel);
	}
	
	private void createModifyPanel(JPanel widgetPanel)
	{
		JPanel modifyPanel = new JPanel();
		modifyPanel.setLayout(new BorderLayout());
		modifyPanel.setBorder(BorderFactory.createEtchedBorder());
		
		JPanel modifyButtonsPanel = new JPanel();
		modifyButtonsPanel.setLayout(new BoxLayout(modifyButtonsPanel,BoxLayout.Y_AXIS));
		modifyPanel.add(modifyButtonsPanel,BorderLayout.SOUTH);
		
		JPanel modifyLoadPanel = new JPanel();
		loadButton = new JButton();
		loadButton.setText("Load");
		loadButton.addActionListener(this);
		modifyLoadPanel.add(loadButton);
		modifyPanel.add(modifyLoadPanel,BorderLayout.NORTH);
		
		JScrollPane animScroll = new JScrollPane();
		animList = new JList();
		animList.addListSelectionListener(this);
		animList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
		animScroll.setViewportView(animList);		
		modifyPanel.add(animScroll,BorderLayout.CENTER);
		
		JPanel editAnimPanel = new JPanel();
		modifyButtonsPanel.add(editAnimPanel);
		addAnimButton = new JButton();
		addAnimButton.setText("Add");
		addAnimButton.addActionListener(this);
		editAnimPanel.add(addAnimButton);
		removeAnimButton = new JButton();
		removeAnimButton.addActionListener(this);
		removeAnimButton.setText("Remove");
		editAnimPanel.add(removeAnimButton);
		
		this.speedIncrease = new JButton("+");
		speedIncrease.addActionListener(this);
		this.speedDecrease = new JButton("-");
		speedDecrease.addActionListener(this);
		JPanel previewModPanel = new JPanel();
		previewModPanel.add(javax.swing.Box.createHorizontalGlue());
		previewModPanel.add(speedDecrease);
		preview = new JLabel();
		previewPanel = new JScrollPane(preview);
		previewPanel.setPreferredSize(new Dimension(40,30));
		previewModPanel.add(previewPanel);
		previewModPanel.add(speedIncrease);
		previewModPanel.add(javax.swing.Box.createHorizontalGlue());
		modifyButtonsPanel.add(previewModPanel);
		previewPanel.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneLayout.HORIZONTAL_SCROLLBAR_NEVER);
		previewPanel.setVerticalScrollBarPolicy(javax.swing.ScrollPaneLayout.VERTICAL_SCROLLBAR_NEVER);				
		preview.setAlignmentX(0.0f);
		preview.setAlignmentY(0.0f);
		preview.setOpaque(true);
		preview.setBackground(Color.BLACK);
		preview.setVerticalAlignment(JLabel.TOP);
		
		JPanel modifySavePanel = new JPanel();
		saveButton = new JButton();
		saveButton.setText("Save");
		saveButton.addActionListener(this);
		modifySavePanel.add(saveButton);
		modifyButtonsPanel.add(modifySavePanel);
		
		widgetPanel.add(modifyPanel);
	}
	
	/**
	 * @param args
	 */
	public static void main(String[] args)
	{
		new SpriteGenerator();
	}

	@Override
	public void actionPerformed(ActionEvent arg0)
	{
		if (arg0.getSource() == this.generateButton)
		{
			generateImage();
		}
		else if (arg0.getSource() == this.loadButton)
		{
			load();
		}
		else if (arg0.getSource() == this.saveButton)
		{
			save();
		}
		else if (arg0.getSource() == this.addAnimButton)
		{	
			addAnim();
		}
		else if (arg0.getSource() == this.removeAnimButton)
		{	
			removeAnim();
		}
		else if (arg0.getSource() == this.statusReset)
		{	
			Animation a = getSelectedAnimation();
			if (a != null)
			{
				a.frames.clear();
				updateStatusText();
			}			
		}
		else if (arg0.getSource() == this.timer)
		{
			updatePreviewAnimation();
		}
		else if (arg0.getSource() == this.speedIncrease)
		{
			spritesheet.fps++;
			if (spritesheet.fps >= 30)
			{
				spritesheet.fps = 30;
			}
			timer.setDelay(1000/spritesheet.fps);
		}
		else if (arg0.getSource() == this.speedDecrease)
		{
			spritesheet.fps--;
			if (spritesheet.fps <= 0)
			{
				spritesheet.fps = 0;
			}
			timer.setDelay(1000/spritesheet.fps);
		}
	}
	
	private void removeAnim()
	{
		if (!this.animList.isSelectionEmpty())
		{
			String name = this.animList.getSelectedValue().toString();
			spritesheet.removeAnimation(name);
			updateList();
		}
	}
	
	private void addAnim()
	{
		String name = JOptionPane.showInputDialog(this,
				"Enter the name of the animation",
				  "Add animation",
				  JOptionPane.QUESTION_MESSAGE);
		
		if (name != null)
		{
			if (spritesheet.addAnimation(name))
			{
				updateList();
				updateStatusText();
			}
			else
			{
				error("There is a category with that name already!");
			}
			
		}
	}
	
	private void updateList()
	{
		DefaultListModel lm = new DefaultListModel();
		for (Animation a : spritesheet.animationList)
		{
			lm.addElement(a.name);
		}
		this.animList.setModel(lm);
	}
	
	private void load()
	{
		int option = fc.showOpenDialog(this);
		if (option == JFileChooser.APPROVE_OPTION)
		{
			try
			{
				// Load the spritesheet
				java.io.File f = fc.getSelectedFile();
				
				// Load the XML file
				flexo.file.File xml = new flexo.file.File();	
				String nakedPath = f.getAbsolutePath();
				nakedPath = nakedPath.substring(0,nakedPath.lastIndexOf('.'));
				xml.setName(nakedPath  + ".xml");
				xml.load(new flexo.file.reader.Xml());
				
				Node root = xml.getRootNode();
				Node spriteNode = root.getChild("sprite");
				spritesheet.name = spriteNode.getProperty("name").getStringValue();
				spritesheet.texWidth = spriteNode.getProperty("texWidth").getIntValue();
				spritesheet.texHeight = spriteNode.getProperty("texHeight").getIntValue();
				spritesheet.frameWidth = spriteNode.getProperty("frameWidth").getIntValue();
				spritesheet.frameHeight = spriteNode.getProperty("frameHeight").getIntValue();
				spritesheet.xFrames = spriteNode.getProperty("xFrames").getIntValue();
				spritesheet.yFrames = spriteNode.getProperty("yFrames").getIntValue();
				spritesheet.fps = spriteNode.getProperty("fps").getIntValue();
				spritesheet.animationList.clear();
								
				for (int i = 0; i < spriteNode.getChildrenCount(); ++i)
				{
					Node aniNode = spriteNode.getChild(i);
					Animation a = new Animation(aniNode.getProperty("name").getStringValue());
					
					for (int j = 0; j < aniNode.getChildrenCount(); ++j)
					{
						Node frameNode = aniNode.getChild(j);
						a.frames.add(new Integer(frameNode.getProperty("text").getIntValue()));			
					}
					spritesheet.animationList.add(a);
				}
				updateList();
				
				// Load the image
				java.io.File image = new java.io.File(nakedPath + ".png");
				previewImage = javax.imageio.ImageIO.read(image);
				updateImageIcon();
				enableButtons(true);
			}
			catch (IOException ioe)
			{
				error("And error ocurred while loading the spritesheet!");
			}
		}
	}
	
	private void save()
	{
		int option = fc.showSaveDialog(this);
		if (option == JFileChooser.APPROVE_OPTION)
		{
			try
			{
				// Save the spritesheet
				java.io.File f = fc.getSelectedFile();
				
				// Save the XML file
				flexo.file.File xml = new flexo.file.File();
				String absPath = f.getAbsolutePath();
				absPath = absPath.substring(0,absPath.lastIndexOf('.'));
				xml.setName(absPath + ".xml");			
				
				Node root = xml.getRootNode();
				Node spriteNode = root.addChild("sprite");
				String fName = f.getName();
				fName = fName.substring(0,fName.lastIndexOf('.'));
				spriteNode.setProperty("name", fName);
				spriteNode.setProperty("texWidth", spritesheet.texWidth);
				spriteNode.setProperty("texHeight", spritesheet.texHeight);
				spriteNode.setProperty("frameWidth", spritesheet.frameWidth);
				spriteNode.setProperty("frameHeight", spritesheet.frameHeight);
				spriteNode.setProperty("xFrames", spritesheet.xFrames);
				spriteNode.setProperty("yFrames", spritesheet.yFrames);
				spriteNode.setProperty("fps", spritesheet.fps);				
								
				for (Animation a : spritesheet.animationList)
				{
					if (a.frames.size() > 0)
					{
						Node aniNode = spriteNode.addChild("animation");
						aniNode.setProperty("name", a.name);
						
						for (int frame: a.frames)
						{
							Node frameNode = aniNode.addChild("frame");
							frameNode.setProperty("text", frame);					
						}
					}
				}
				
				xml.save(new flexo.file.writer.Xml());
				
				// Save the image
				java.io.File image = new java.io.File(f.getAbsolutePath() + ".png");
				javax.imageio.ImageIO.write(previewImage, "png", image);					
			}
			catch (IOException ioe)
			{
				error("And error ocurred while saving the spritesheet!");
			}
		}
	}
	
	private void error(String error)
	{
		JOptionPane.showMessageDialog(this, error, "Error ocurred", JOptionPane.PLAIN_MESSAGE);
	}	

	@Override
	public void mouseEntered(MouseEvent arg0){}
	@Override
	public void mouseExited(MouseEvent arg0){}
	@Override
	public void mousePressed(MouseEvent arg0){}
	@Override
	public void mouseReleased(MouseEvent arg0){}
	
	@Override
	public void mouseClicked(MouseEvent arg0)
	{
		if (!this.animList.isSelectionEmpty())
		{
			Point mousePos = arg0.getPoint();
			// Inside the texture
			if (mousePos.x < spritesheet.texWidth && mousePos.y < spritesheet.texHeight)
			{
				int x,y;
				
				x = mousePos.x / spritesheet.frameWidth;
				y = mousePos.y / spritesheet.frameHeight;
				
				int pos = (y * spritesheet.xFrames) + x;
				
				Animation a = getSelectedAnimation();
				if (a != null)
				{
					a.frames.add(new Integer(pos));
					updateStatusText();
				}											
			}						
		}		
	}
	
	private Animation getSelectedAnimation()
	{
		if (!animList.isSelectionEmpty())
		{
			return spritesheet.getAnimation(animList.getSelectedValue().toString());
		}
		return null;
	}
	
	private void updateStatusText()
	{
		Animation a = getSelectedAnimation();
		if (a !=null)
		{
			statusText.setText(a.toString());
		}
		else
		{
			statusText.setText("");
		}
	}

	@Override
	public void valueChanged(ListSelectionEvent arg0)
	{
		if (this.animList.isSelectionEmpty())
		{
			timer.stop();
		}
		else
		{
			this.previewFrameNumber = 0;
			timer.start();
			updateStatusText();
		}
	}
	
	private void updatePreviewAnimation()
	{
		Animation a = getSelectedAnimation();
		if (a != null && a.frames.size() > 0)
		{
			if (this.previewFrameNumber >= a.frames.size())
			{
				this.previewFrameNumber = 0;			
			}
			Integer frame = a.frames.get(this.previewFrameNumber++);
			int frm = 0;
			if (frame != null)
			{
				frm = frame.intValue();
			}
			
			if (this.previewFrameNumber >= a.frames.size())
			{
				this.previewFrameNumber = 0;			
			}
			
			int x = frm % spritesheet.xFrames;
			int y = frm / spritesheet.xFrames;
			
			Point upperLeft = new Point(x * spritesheet.frameWidth,y * spritesheet.frameHeight);
			previewPanel.getViewport().setViewPosition(upperLeft);
			preview.repaint();
		}
	}
}
